/**
 * 请勿将俱乐部专享资源复制给其他人，保护知识产权即是保护我们所在的行业，进而保护我们自己的利益
 * 即便是公司的同事，也请尊重 JFinal 作者的努力与付出，不要复制给同事
 * 
 * 如果你尚未加入俱乐部，请立即删除该项目，或者现在加入俱乐部：http://jfinal.com/club
 * 
 * 俱乐部将提供 jfinal-club 项目文档与设计资源、专用 QQ 群，以及作者在俱乐部定期的分享与答疑，
 * 价值远比仅仅拥有 jfinal club 项目源代码要大得多
 * 
 * JFinal 俱乐部是五年以来首次寻求外部资源的尝试，以便于有资源创建更加
 * 高品质的产品与服务，为大家带来更大的价值，所以请大家多多支持，不要将
 * 首次的尝试扼杀在了摇篮之中
 */

package com.tianpengtech.front.web.login;

import com.jfinal.kit.HashKit;
import com.jfinal.kit.PropKit;
import com.jfinal.kit.Ret;
import com.jfinal.kit.StrKit;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Record;
import com.jfinal.plugin.ehcache.CacheKit;
import com.tianpengtech.common.ip.IpInfo;
import com.tianpengtech.common.ip.IpUtil;
import com.tianpengtech.common.model.user.Session;
import com.tianpengtech.common.model.user.User;

import java.util.Date;
import java.util.List;

/**
 * 登录业务
 */
public class LoginService {

	public static final LoginService me = new LoginService();
	private final User userDao = new User().dao();

	// 存放登录用户的 cacheName
	public static final String loginAccountCacheName = "loginAccount";

	// "jfinalId" 仅用于 cookie 名称，其它地方如 cache 中全部用的 "sessionId" 来做 key
	public static final String sessionIdName = "fairId";

	//用户密码错误次数 缓存key
	public static final String ERRORCOUNT_KEY = "error_count_";

	//用户错误密码次数 缓存名称
	public static final String ERRORCOUNT_CACHENAME = "errorcount";

	/**
	 * 登录成功返回 sessionId 与 loginAccount，否则返回一个 msg
	 */
	public Ret login(String mobile, String password, boolean keepLogin, String loginIp) {
		password = password.trim();
		User user = findByMobile(mobile,User.USER_TYPE_PROV);
		if (user == null) {
			return Ret.fail("msg", "用户名不存在");
		}
		if (user.isPwdErrorLockId()){
			Integer error=CacheKit.get(ERRORCOUNT_CACHENAME,ERRORCOUNT_KEY+mobile);
			if (null == error){
				user.set(User.STATUS,User.USER_STATUS_NORMAL);
				user.update();
			}else{
				return Ret.fail("msg", "密码错误次数过多账号已被锁定");
			}
		}
		if (user.isStatusLockId()) {
			return Ret.fail("msg", "账号已被锁定");
		}
		String salt = user.get(User.SALT);
		String hashedPass = HashKit.sha256(salt + password);
		// 未通过密码验证
		if (user.get(User.PASSWORD).equals(hashedPass) == false) {
			//加入防止密码暴力破解， 加入连续输入密码错误锁定账号的操作
			Integer error=CacheKit.get(ERRORCOUNT_CACHENAME,ERRORCOUNT_KEY+mobile);
			if (null == error){
				error=0;
			}
			Integer staticErrorCount=PropKit.getInt("user.password.errorcount",5);
			//判断是否与设置的错误次数少， 少就加一 如果相等 就锁定账号
			if (error.intValue()<staticErrorCount.intValue()){
				error++;
				CacheKit.put(ERRORCOUNT_CACHENAME,ERRORCOUNT_KEY+mobile,error);
			}else{
				CacheKit.remove(ERRORCOUNT_CACHENAME,ERRORCOUNT_KEY+mobile);
				user.set(User.STATUS,User.USER_STATUS_PWD_ERROR_LOCK);
				user.update();
				return Ret.fail("msg", "账号密码错误次数过多已被锁定");
			}
			return Ret.fail("msg", "密码不正确");
		}

		// 如果用户勾选保持登录，暂定过期时间为 3 年，否则为 120 分钟，单位为秒
		long liveSeconds =  keepLogin ? 3 * 365 * 24 * 60 * 60 : 120 * 60;
		// 传递给控制层的 cookie
		int maxAgeInSeconds = (int)(keepLogin ? liveSeconds : -1);
		// expireAt 用于设置 session 的过期时间点，需要转换成毫秒
		long expireAt = System.currentTimeMillis() + (liveSeconds * 1000);
		// 保存登录 session 到数据库
		Session session = new Session();
		String sessionId = StrKit.getRandomUUID();
		session.set(Session.ID,sessionId);
		session.set(Session.USERID,user.get(User.ID));
		session.set(Session.EXPIREAT,expireAt);
		if ( ! session.save()) {
			return Ret.fail("msg", "保存 session 到数据库失败，请联系管理员");
		}

		user.removeSensitiveInfo();                                 // 移除 password 与 salt 属性值
		user.put("sessionId", sessionId);                          // 保存一份 sessionId 到 loginAccount 备用
		CacheKit.put(loginAccountCacheName, sessionId, user);

		createLoginLog(user.getLong(User.ID), loginIp,"web");

		return Ret.ok(sessionIdName, sessionId)
						.set(loginAccountCacheName, user)
						.set("maxAgeInSeconds", maxAgeInSeconds);   // 用于设置 cookie 的最大存活时间
	}

	public User getLoginAccountWithSessionId(String sessionId) {
		return CacheKit.get(loginAccountCacheName, sessionId);
	}

	/**
	 * 通过 sessionId 获取登录用户信息
	 * sessoin表结构：session(id, accountId, expireAt)
	 *
	 * 1：先从缓存里面取，如果取到则返回该值，如果没取到则从数据库里面取
	 * 2：在数据库里面取，如果取到了，则检测是否已过期，如果过期则清除记录，
	 *     如果没过期则先放缓存一份，然后再返回
	 */
	public User loginWithSessionId(String sessionId, String loginIp) {
		Session session = Session.dao.findById(sessionId);
		if (session == null) {      // session 不存在
			return null;
		}
		if (session.isExpired()) {  // session 已过期
			session.delete();		// 被动式删除过期数据，此外还需要定时线程来主动清除过期数据
			return null;
		}
		User user = userDao.findById(session.getInt(Session.USERID));
		// 找到 loginAccount 并且 是正常状态 才允许登录
		if (user != null && user.isStatusOk()) {
			user.removeSensitiveInfo();                                 // 移除 password 与 salt 属性值
			user.put("sessionId", sessionId);                          // 保存一份 sessionId 到 loginAccount 备用
			CacheKit.put(loginAccountCacheName, sessionId, user);
			createLoginLog(user.getLong(User.ID), loginIp,"web");
			return user;
		}
		return null;
	}

	/**
	 * 创建登录日志
	 */
	public void createLoginLog(Long accountId, String loginIp,String source) {
		IpInfo info=IpUtil.getIpInfo(loginIp);
		Record loginLog = new Record().set("userId", accountId).set("ip", loginIp).set("loginAt", new Date()).set("province",info.getProvince()).set("city",info.getCity()).set("country",info.getCountry()).set("part",info.getPart()).set("source",source);
		Db.save("fair_login_log", loginLog);
	}

	/**
	 * 查询已过期的session信息
	 * @return
	 */
	public List<Session> findEndTimeSession(){
		return Session.dao.find("select * from "+Session.dao.tableName()+" where expireAt<?",System.currentTimeMillis());
	}

	/**
	 * 退出登录
	 */
	public void logout(String sessionId) {
		if (sessionId != null) {
			CacheKit.remove(loginAccountCacheName, sessionId);
			Session.dao.deleteById(sessionId);
		}
	}

	/**
	 * 从数据库重新加载登录账户信息
	 */
	public void reloadLogin(User user) {
		String sessionId = user.get("sessionId");
		User newUser = user.findById(user.get(User.ID));
		newUser.removeSensitiveInfo();               // 移除 password 与 salt 属性值
		newUser.put("sessionId", sessionId);        // 保存一份 sessionId 到 loginAccount 备用
		// 集群方式下，要做一通知其它节点的机制，让其它节点使用缓存更新后的数据，
		// 将来可能把 account 用 id : obj 的形式放缓存，更新缓存只需要 CacheKit.remove("account", id) 就可以了，
		// 其它节点发现数据不存在会自动去数据库读取，所以未来可能就是在 AccountService.getById(int id)的方法引入缓存就好
		// 所有用到 account 对象的地方都从这里去取
		CacheKit.put(loginAccountCacheName, sessionId, newUser);
	}

	/**
	 * 根据手机号码查询 用户信息
	 */
	public User findByMobile(String mobile,Integer user_type){
		String sql =String.format("select * from %s where %s = ? and %s = ? and %s=?",new User().tableName(),User.MOBILE,User.DELETETAG,User.USER_TYPE);
		User user=userDao.findFirst(sql,mobile,User.DELETE_STATUS_NO,user_type);
		if (null == user){
			return null;
		}else{
			return user;
		}
	}

	/**
	 * 根据邀请码查询 用户信息
	 */
	public User findByCode(String code,Integer user_type){
		String sql =String.format("select * from %s where %s = ? and %s = ? and %s=?",new User().tableName(),User.CODE,User.DELETETAG,User.USER_TYPE);
		User user=userDao.findFirst(sql,code,User.DELETE_STATUS_NO,user_type);
		if (null == user){
			return null;
		}else{
			return user;
		}
	}

}
